﻿using System;
using System.IO;
using System.Collections.Generic;

namespace FileTransformer
{
    public delegate string FilePartNameGiver(int index);

    public class NameToLongException : Exception { }

    public class WrongHeaderException : Exception { }

    public class UnexpectedFileEnd : Exception
    {
        public UnexpectedFileEnd(string fileName)
            : base(string.Format("file <{0}>", fileName))
        { }
    }

    public class FileItem
    {
        public string Name;
        public int Length;
    }

    public class Descripter
    {
        private readonly List<FileItem> _files;
        private readonly List<string> _arxFiles;

        public Identity OutIdentity { get; private set; }

        public void AddArx(string name)
        {
            _arxFiles.Add(name);
        }

        public int FileCount
        {
            get { return _files.Count; }
        }

        public int ArxCount
        {
            get { return _arxFiles.Count; }
        }

        public IEnumerable<string> GetArxEnumerator()
        {
            foreach (var item in _arxFiles)
                yield return item;
        }

        public Descripter()
        {
            _files = new List<FileItem>();
            _arxFiles = new List<string>();
        }

        public Descripter(byte[] data, Action<string> addNameAction)
            : this()
        {
            if (data == null || data.Length < 48)
                throw new WrongHeaderException();

            var stream = new MemoryStream(data);

            var key = new byte[32];
            var iv = new byte[16];
            stream.Seek(0, SeekOrigin.Begin);
            stream.Read(key, 0, 32);
            stream.Read(iv, 0, 16);
            var identity = new Identity(key, iv);

            var header = new byte[stream.Length - 32 - 16];
            stream.Read(header, 0, header.Length);
            try
            {
                stream = new MemoryStream(DataUtils.DecompressAndDecript(header, identity));

                var count = stream.HReadInt();
                _files.Clear();
                for (var i = 0; i < count; i++)
                {
                    var item = new FileItem { Length = stream.HReadInt(), Name = stream.HReadName() };
                    _files.Add(item);

                    if (addNameAction != null)
                        addNameAction(item.Name);
                }

                count = stream.HReadInt();
                _arxFiles.Clear();
                for (var i = 0; i < count; i++)
                    _arxFiles.Add(stream.HReadName());
            }
            catch (Exception)
            {
                throw new WrongHeaderException();
            }

            OutIdentity = identity;
        }

        public IEnumerable<FileItem> FilesEnumerator()
        {
            foreach (var item in _files)
                yield return item;
        }

        private byte[] ComposeHeader(Identity identity)
        {
            var stream = new MemoryStream();
            stream.HWrite(_files.Count);
            foreach (var item in _files)
            {
                stream.HWrite(item.Length);
                stream.HWrite(item.Name);
            }

            stream.HWrite(_arxFiles.Count);
            foreach (var name in _arxFiles)
                stream.HWrite(name);

            return DataUtils.CompressAndCript(stream.ToArray(), identity);
        }

        public void SaveToFile(string fileName, Identity identity)
        {
            using (var stream = new MemoryStream())
            {
                // TODO: real save
                stream.HWrite(identity.Compose(null));
                stream.HWrite(ComposeHeader(identity));
                stream.HSave(fileName);
            }
        }

        public byte[] LoadSources(DataSourceList list)
        {
            var stream = new MemoryStream();
            foreach (var pair in list)
            {
                var bytes = pair.Value.GetData();
                stream.HWrite(bytes);
                _files.Add(new FileItem { Name = pair.Key, Length = bytes.Length });
            }
            return stream.ToArray();
        }
    }

    public class DataProcessor
    {
        private static void Divide(byte[] bytes, int partSize, FilePartNameGiver nameGiver, Descripter descripter)
        {
            if (nameGiver == null)
                throw new ArgumentNullException("nameGiver");

            if (partSize == 0)
            {
                var name = nameGiver(1);
                descripter.AddArx(name);
                DataUtils.SaveBytesToFile(name, bytes);
                return;
            }

            if (partSize < 50)
                throw new ArgumentException("partSize");

            var position = 0;
            var partIndex = 1;
            while (position < bytes.Length)
            {
                var arxName = nameGiver(partIndex);
                descripter.AddArx(FileUtils.GetFileName(arxName));
                DataUtils.SaveBytesToFile(arxName, bytes, position, partSize);
                position += partSize;
                partIndex++;
            }
        }

        private static void ExtractData(byte[] data, string destination, Descripter descripter)
        {
            if (!Directory.Exists(destination))
                throw new ArgumentException("destination");

            destination = FileUtils.GetFileDirectoryPath(destination);
            var stream = new MemoryStream(data);
            foreach (var item in descripter.FilesEnumerator())
            {
                var fileBytes = new byte[item.Length];
                var readCount = stream.Read(fileBytes, 0, item.Length);
                if (readCount != item.Length)
                    throw new UnexpectedFileEnd(item.Name);

                var path = destination + item.Name;
                FileUtils.CreateDirectory(FileUtils.GetDirectoryName(path));
                DataUtils.SaveBytesToFile(path, fileBytes);
            }
        }

        public static void RunDown(DataSourceList list, int partSize, FilePartNameGiver nameGiver)
        {
            if (nameGiver == null)
                throw new ArgumentNullException("nameGiver");

            var descripter = new Descripter();
            var origin = descripter.LoadSources(list);

            var identity = Identity.Generate();
            var bytes = DataUtils.CompressAndCript(origin, identity);

            Divide(bytes, partSize, nameGiver, descripter);

            descripter.SaveToFile(nameGiver(0), identity);
        }

        public static Descripter ReadHeader(string headerFile, Action<string> addNameAction = null)
        {
            return new Descripter(DataUtils.ReadFromFile(headerFile), addNameAction);
        }

        public static void Extract(string destination, string basis, Descripter descripter)
        {
            byte[] data;
            using (var stream = new MemoryStream())
            {
                foreach (var file in descripter.GetArxEnumerator())
                    stream.HWrite(DataUtils.ReadFromFile(basis + file));

                data = stream.ToArray();
            }

            data = DataUtils.DecompressAndDecript(data, descripter.OutIdentity);
            ExtractData(data, destination, descripter);
        }
    }
}
